using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using SpellCenter.Core;
using System.Text.RegularExpressions;
using SpellCenter.UI.Properties;

namespace SpellCenter.UI.FileChoosing
{
    public class FileTreeControlPM
    {
        private readonly DirectoryInfo _dir;
        private readonly TreeNodeCollection _nodeList;
        private readonly Dictionary<TreeNode, DirectoryInfo> _stubs;
        private readonly TriStateTreeView _tree;
        private readonly List<Regex> _ignoreList;
        private readonly ImageList _imageList;

        internal FileTreeControlPM(DirectoryInfo dir, TriStateTreeView tree, bool recursive)
        {
            tree.Model = this;
            _dir = dir;
            _tree = tree;
            _nodeList = tree.Nodes;
            _stubs = new Dictionary<TreeNode, DirectoryInfo>();
            _imageList = new ImageList();
            _ignoreList = new List<Regex>();
            InitImageList();
            _tree.ImageList = _imageList;
            if (_dir.Exists)
            {
                TreeNode root = _nodeList.Add(_dir.Name, _dir.Name);
                root.Tag = dir;
                FillNode(root, dir, recursive);
                root.Expand();
            }
        }

        private void InitImageList()
        {
            _imageList.Images.Add("error", SystemIcons.Exclamation);
            _imageList.Images.Add("folderOpen", Resources.FolderOpen);
            _imageList.Images.Add("folderClose", Resources.FolderClose);
            _imageList.Images.Add("file", Resources.File);
            _imageList.Images.Add(".ptx", Resources.paratext);
            _tree.AfterCollapse += Tree_FolderCollapseOrExpand;
            _tree.AfterExpand += Tree_FolderCollapseOrExpand;
        }

        private static void Tree_FolderCollapseOrExpand(object sender, TreeViewEventArgs e)
        {
            if (e.Node.Tag is DirectoryInfo)
            {
                string imgkey = ((e.Action & TreeViewAction.Expand) > 0 ? "folderOpen" : "folderClose");
                e.Node.ImageKey = imgkey;
                e.Node.SelectedImageKey = imgkey;
            }
        }

        /// <summary>
        /// Initializes a FileTreeControlPM  with a tree that has "Desktop" on top with
        /// "My Computer" below that with all physical drives.  The tree is loaded lazily, and
        /// so ExpandNode(TreeNode) should be called every time it needs to be expanded.  This can be
        /// done by calling ExpandNode(e.Node) in a handler for the TreeView.BeforeExpand event.
        /// </summary>
        /// <param name="tree">The TreeView to fill with data</param>
        public FileTreeControlPM(TriStateTreeView tree)
            :this(new DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.Desktop)), tree, false)
        {
            _tree = tree;
            InitializeWithMyComputer();
        }

        public void InitializeWithMyComputer()
        {
            TreeNode mycomputer = _nodeList.Add("\\", "My Computer");
            _imageList.Images.Add("MyComputer", Resources.MyComputer);
            mycomputer.ImageKey = "MyComputer";
            mycomputer.SelectedImageKey = "MyComputer";

            DirectoryInfo mydocs = new DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
            TreeNode mydocsNode = AddNode(mycomputer, mydocs);
            CreateStub(mydocsNode, mydocs);
            _imageList.Images.Add("drive", Resources.drive);
            foreach (string drive in Environment.GetLogicalDrives())
            {
                DirectoryInfo driveInfo = new DirectoryInfo(drive);
                
                // Check if the drive is ready, if not, don't include it
                try {driveInfo.GetFileSystemInfos();}
                catch(IOException){ continue; }

                TreeNode node = mycomputer.Nodes.Add(drive, drive);
                CreateStub(node, driveInfo);
                node.ImageKey = "drive";
                node.SelectedImageKey = "drive";
            }
            mycomputer.Expand();
        }

        /// <summary>
        /// Add a pattern that matches filenames to leave unchecked by default. The pattern should
        /// match the string returned from FileSystemInfo.Name (no path).  If this matches a directory,
        /// that directory and everything under it will remain unchecked. Also, it only has to match part
        /// of the pattern
        /// 
        /// For instance, to ignore all .svn directories, pass in a @"^\.svn$" Regex
        /// </summary>
        /// <param name="pattern"></param>
        public void AddToIgnoreList(Regex pattern)
        {
            _ignoreList.Add(pattern);
        }
        internal bool ShouldBeIgnored(string filename)
        {
            foreach (Regex pattern in _ignoreList)
            {
                if(pattern.IsMatch(filename))
                    return true;
            }
            return false;
        }

        private void FillNode(TreeNode node, DirectoryInfo dir, bool recursive)
        {
            Debug.Assert(node != null);
            foreach (FileSystemInfo info in dir.GetFileSystemInfos())
            {
                TreeNode child = AddNode(node, info);
                if(info is DirectoryInfo)
                {
                    if(recursive)
                    {
                        FillNode(child, (DirectoryInfo) info, true);
                    }
                    else
                    {
                        CreateStub(child, (DirectoryInfo) info);
                    }
                }
            }
        }

        private void CreateStub(TreeNode child, DirectoryInfo info)
        {
            child.Nodes.Add("",""); //Now has children
            _stubs.Add(child, info);
        }

        /// <summary>
        /// Expand a node to the next level and create a temporary stub child under each directory.
        /// </summary>
        /// <param name="stub"></param>
        public void ExpandNode(TreeNode stub)
        {
            if(_stubs.ContainsKey(stub))
            {
                Debug.Assert(stub.Nodes.Count == 1);
                stub.Nodes.RemoveAt(0);
                FillNode(stub, _stubs[stub], false);
                _stubs.Remove(stub);
            }
        }

        private TreeNode AddNode(TreeNode parent, FileSystemInfo file)
        {
            TreeNode ret = parent.Nodes.Add(file.Name, file.Name);
            ret.Tag = file;
            if ((parent.StateImageIndex == TriStateTreeView.STATE_UNCHECKED ||
                parent.StateImageIndex == TriStateTreeView.STATE_CHECKED) && !ShouldBeIgnored(file.Name))
            {
                ret.StateImageIndex = parent.StateImageIndex;
            }
            else ret.StateImageIndex = TriStateTreeView.STATE_UNCHECKED;
            SetImageIcon(ret, file);
            return ret;
        }

        private void SetImageIcon(TreeNode ret, FileSystemInfo file)
        {
            if(file is FileInfo)
            {
                string ext = file.Extension.ToLower(); //force all same case (dictionaries recognize case)
                string key = "file";
                if(ext == ".ptx")
                {   // force our own icon
                    key = ext;
                }
                else if (!_imageList.Images.ContainsKey(ext))
                {
                    try
                    {
                        _imageList.Images.Add(ext, Icon.ExtractAssociatedIcon(file.FullName));
                        key = ext;
                    }
                    catch
                    {
                        key = "error";
                    }
                }
                ret.ImageKey = key;
                ret.SelectedImageKey = key;
            }
            else if(file is DirectoryInfo)
            {
                ret.ImageKey = "folderClose";
                ret.SelectedImageKey = "folderClose";
            }
        }

        public TreeNodeCollection NodeList
        {
            get { return _nodeList; }
        }

        public DirectoryInfo Directory
        {
            get { return _dir; }
        }

        public IEnumerable<FileInfo> GetIncludedFiles()
        {
            return GetAllChildrenOfCheck(_nodeList, TriStateTreeView.STATE_CHECKED);
        }

        private IEnumerable<FileInfo> GetAllChildrenOfCheck(TreeNodeCollection nodes, int check)
        {
            foreach (TreeNode child in nodes)
            {
                if (child.StateImageIndex == check && child.Tag is FileInfo)
                {
                    yield return (FileInfo)child.Tag;
                }
                if (child.StateImageIndex == TriStateTreeView.STATE_CHECKED || 
                    child.StateImageIndex == TriStateTreeView.STATE_MIXED)
                {
                    ExpandNode(child);
                    foreach (FileInfo tag in GetAllChildrenOfCheck(child.Nodes, check))
                    {
                        yield return tag;
                    }
                }
            }
        }

        /// <summary>
        /// Finds all files in the <code>TreeView</code> that are under a checked node (directory)
        /// but are themselves unchecked (they are included but more specifically excluded).
        /// </summary>
        /// <returns></returns>
        public IEnumerable<FileInfo> GetExcludedFiles()
        {
            return GetAllChildrenOfCheck(_nodeList, TriStateTreeView.STATE_UNCHECKED);
        }

        public void UncheckFileType(string type)
        {
            ForceCheckedOfFileType(type, false, _nodeList["\\"], _dir);
        }
        public void CheckFileType(string type)
        {
            ForceCheckedOfFileType(type, true, _nodeList["\\"], _dir);
        }

        private static void ForceCheckedOfFileType(string type, bool check, TreeNode parent, DirectoryInfo dir)
        {
            Debug.Assert(dir.Name == parent.Name || parent.Name == "\\");
            foreach (FileInfo file in dir.GetFiles("*"+type))
            {
                parent.Nodes[file.Name].Checked = check;
            }
            foreach (DirectoryInfo info in dir.GetDirectories())
            {
                Debug.Assert(parent.Nodes[info.Name] != null);
                ForceCheckedOfFileType(type, check, parent.Nodes[info.Name], info);
            }
        }

        public void LoadConfigFile(FilesInCorpusList value)
        {
            CheckFilePatterns(value.IncludePatterns, true);
            CheckFilePatterns(value.ExcludePatterns, false);
        }

        private void CheckFilePatterns(IEnumerable<FilePattern> patterns, bool check)
        {
            Dictionary<string, bool> fileset = GetFileset(patterns);
            foreach (TreeNode node in _nodeList)
            {
                CheckAllNodesInSet(node, fileset, check);
            }
        }

        private void CheckAllNodesInSet(TreeNode node, IDictionary<string, bool> set, bool check)
        {
            if (node.Tag is FileSystemInfo)
            {
                FileSystemInfo info = (FileSystemInfo)node.Tag;
                if(set.ContainsKey(info.FullName))
                {
                    if(set[info.FullName]) //file was matched by a filepattern
                    {
                        if(check)
                            _tree.CheckNode(node);
                        else _tree.UncheckNode(node);
                    }
                    ExpandNode(node); //lazy load
                }
            }
            foreach (TreeNode td in node.Nodes)
            {
                Debug.Assert(!(node.Tag is FileInfo));
                CheckAllNodesInSet(td, set, check);
            }
        }

        /// <summary>
        /// Get a set that we can use to lookup things later.  
        /// 
        /// Dictionary type parameters are as follows:
        /// string - a filesystem entry
        /// bool - true if it matched a file pattern and false if it's a parent directory of a 
        /// matching filesystem entry.
        /// </summary>
        /// <param name="patterns"></param>
        /// <returns></returns>
        private static Dictionary<string, bool> GetFileset(IEnumerable<FilePattern> patterns)
        {
            Dictionary<string, bool> fileset = new Dictionary<string, bool>();
            foreach (FilePattern pattern in patterns)
            {
                foreach (string file in System.IO.Directory.GetFiles(pattern.Directory, pattern.Pattern))
                {
                    fileset[file] = true;
                }
                AddAllParentDirectoriesToSet(pattern.Directory, fileset);
            }
            return fileset;
        }

        private static void AddAllParentDirectoriesToSet(string directory, IDictionary<string, bool> fileset)
        {
            DirectoryInfo curr = System.IO.Directory.CreateDirectory(directory);
            while(curr.Parent != null)
            {
                if(!fileset.ContainsKey(curr.FullName))
                {
                    fileset.Add(curr.FullName, false);
                }
                else return; // we should have all these directories

                curr = curr.Parent;
            }
        }
    }
}
